package com.dny.asmtop.op;

import com.dny.asmtop.ASMMethodUtils;
import com.dny.asmtop.Command;
import com.dny.asmtop.MethodContext;
import com.dny.asmtop.Primitives;
import jdk.internal.org.objectweb.asm.Type;
import jdk.internal.org.objectweb.asm.commons.GeneratorAdapter;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Objects;

import static java.lang.Character.toUpperCase;
import static java.lang.String.format;
import static java.lang.reflect.Modifier.isPublic;
import static java.lang.reflect.Modifier.isStatic;
import static jdk.internal.org.objectweb.asm.Type.getType;
import static jdk.internal.org.objectweb.asm.commons.Method.getMethod;

/**
 * Created by jlutt on 2018-01-16.
 * <br>
 * 获取字段的值
 * <p>
 * 限制为public字段，或有有get，is方法的字段，并且get/is后第一个字母大写
 * </p>
 *
 * @author jlutt
 */
public class VarField implements Variable {

  /**
   * 被调用对象
   */
  private final Command owner;

  private final String fieldName;

  public VarField(Command owner, String fieldName) {
    this.owner = owner;
    this.fieldName = fieldName;
  }

  @Override
  public Type type(MethodContext context) {
    return typeOfFieldGetter(context, owner.type(context), fieldName);
  }

  @Override
  public Type generator(MethodContext context) {
    Type ownerType = owner.generator(context);
    return generatorOfFieldGetter(context, ownerType, fieldName);
  }

  @Override
  public Object beginStore(MethodContext context) {
    return owner.generator(context);
  }

  @Override
  public void store(MethodContext context, Object storeContext, Type type) {
    setField(context, (Type) storeContext, fieldName, type);
  }

  private static Type generatorOfFieldGetter(MethodContext context, Type ownerType, String field) {
    return fieldGetter(context, ownerType, field, true);
  }

  private static Type typeOfFieldGetter(MethodContext context, Type ownerType, String field) {
    return fieldGetter(context, ownerType, field, false);
  }

  private static Type fieldGetter(MethodContext context, Type ownerType, String field, boolean load) {
    GeneratorAdapter g = load ? context.getGeneratorAdapter() : null;

    //如果调用对象==主对象
    if (ownerType.equals(context.getThisType())) {
      Class<?> thisFieldClass = context.getFields().get(field);
      if (thisFieldClass != null) {
        Type resultType = getType(thisFieldClass);
        if (g != null) {
          g.getField(ownerType, field, resultType);
        }
        return resultType;
      } else {
        throw new RuntimeException(format("No public field or getter for class %s for field \"%s\". %s",
            ownerType.getClassName(),
            field,
            ASMMethodUtils.exceptionInGeneratedClass(context)));
      }
    }

    //其他对象，需要反射获取字段信息
    Class<?> argumentClass = ASMMethodUtils.getJavaType(context.getClassLoader(), ownerType);

    try {
      Field javaField = argumentClass.getField(field);
      if (isPublic(javaField.getModifiers()) && !isStatic(javaField.getModifiers())) {
        Type resultType = getType(javaField.getType());
        if (g != null) {
          g.getField(ownerType, field, resultType);
        }
        return resultType;
      }
    } catch (NoSuchFieldException ignored) {
    }

    java.lang.reflect.Method m = null;
    try {
      m = argumentClass.getDeclaredMethod(field);
    } catch (NoSuchMethodException ignored) {
    }

    if (m == null && field.length() >= 1) {
      try {
        m = argumentClass.getDeclaredMethod("get" + toUpperCase(field.charAt(0)) + field.substring(1));
      } catch (NoSuchMethodException ignored) {
      }
    }

    //get找不到，找下is
    if (m == null && field.length() >= 1) {
      try {
        m = argumentClass.getDeclaredMethod("is" + toUpperCase(field.charAt(0)) + field.substring(1));
      } catch (NoSuchMethodException ignored) {
      }
    }

    if (m != null) {
      Type resultType = getType(m.getReturnType());
      if (g != null) {
        ASMMethodUtils.invokeVirtualOrInterface(g, argumentClass, getMethod(m));
      }
      return resultType;
    }

    throw new RuntimeException(format("No public field or getter for class %s for field \"%s\". %s",
        ownerType.getClassName(),
        field,
        ASMMethodUtils.exceptionInGeneratedClass(context)));
  }

  private static void setField(MethodContext context, Type ownerType, String field, Type valueType) {
    GeneratorAdapter g = context.getGeneratorAdapter();

    Class<?> valueClass = ASMMethodUtils.getJavaType(context.getClassLoader(), valueType);

    if (ownerType.equals(context.getThisType())) {
      Class<?> fieldClass = context.getFields().get(field);
      if (fieldClass == null) {
        throw new RuntimeException(format("No field \"%s\" in generated class %s. %s",
            field,
            context.getThisType().getClassName(),
            ASMMethodUtils.exceptionInGeneratedClass(context)));
      }
      Type fieldType = getType(fieldClass);
      ASMMethodUtils.checkCast(context, valueType, fieldType);
      g.putField(ownerType, field, fieldType);
      return;
    }

    Class<?> argumentClass = ASMMethodUtils.getJavaType(context.getClassLoader(), ownerType);

    try {
      java.lang.reflect.Field javaField = argumentClass.getField(field);
      if (Modifier.isPublic(javaField.getModifiers())) {
        Type fieldType = getType(javaField.getType());
        ASMMethodUtils.checkCast(context, valueType, fieldType);
        g.putField(ownerType, field, fieldType);
        return;
      }
    } catch (NoSuchFieldException ignored) {
    }

    java.lang.reflect.Method javaSetter = tryFindSetter(argumentClass, field, valueClass);

    if (javaSetter == null && Primitives.isWrapperType(valueClass)) {
      javaSetter = tryFindSetter(argumentClass, field, Primitives.unwrap(valueClass));
    }

    if (javaSetter == null && valueClass.isPrimitive()) {
      javaSetter = tryFindSetter(argumentClass, field, Primitives.wrap(valueClass));
    }

    if (javaSetter == null) {
      javaSetter = tryFindSetter(argumentClass, field);
    }

    if (javaSetter != null) {
      Type fieldType = getType(javaSetter.getParameterTypes()[0]);
      ASMMethodUtils.checkCast(context, valueType, fieldType);
      ASMMethodUtils.invokeVirtualOrInterface(g, argumentClass, getMethod(javaSetter));
      Type returnType = getType(javaSetter.getReturnType());
      if (returnType.getSize() == 1) {
        g.pop();
      } else if (returnType.getSize() == 2) {
        g.pop2();
      }
      return;
    }

    throw new RuntimeException(format("No public field or setter for class %s for field \"%s\". %s ",
        ownerType.getClassName(),
        field,
        ASMMethodUtils.exceptionInGeneratedClass(context))
    );
  }

  private static Method tryFindSetter(Class<?> argumentClass, String field, Class<?> valueClass) {
    Method m = null;
    try {
      m = argumentClass.getDeclaredMethod(field, valueClass);
    } catch (NoSuchMethodException ignored) {
    }

    if (m == null && field.length() >= 1) {
      try {
        m = argumentClass.getDeclaredMethod("set" + toUpperCase(field.charAt(0)) + field.substring(1), valueClass);
      } catch (NoSuchMethodException ignored) {
      }
    }
    return m;
  }

  private static Method tryFindSetter(Class<?> argumentClass, String field) {
    String setterName = "set" + toUpperCase(field.charAt(0)) + field.substring(1);

    for (Method method : argumentClass.getDeclaredMethods()) {
      if (method.getParameterTypes().length != 1)
        continue;
      if (method.getName().equals(field) || method.getName().equals(setterName))
        return method;
    }
    return null;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    VarField varField = (VarField) o;
    return Objects.equals(owner, varField.owner) &&
        Objects.equals(fieldName, varField.fieldName);
  }

  @Override
  public int hashCode() {
    return Objects.hash(owner, fieldName);
  }
}
